Async I_O
Async I/O means:
Performing input/output without blocking the thread while waiting for the OS.
Instead of:
- Waiting for data
- Parking the thread
We:
- Ask the OS to notify us when data is ready
- Yield control
- Resume later
This is the foundation of scalable async systems.
Why blocking I/O does not scale
Blocking I/O (classic model)
let n = socket.read(&mut buf); // blocks thread
Problems:
- One blocked thread per connection
- Thousands of connections = thousands of threads
- High memory + context switch overhead
Async I/O model
let n = socket.read(&mut buf).await; // yields
Benefits:
- One thread can manage thousands of sockets
- No wasted threads
- Predictable latency
How async I/O works at the OS level
Rust does not magically make I/O async.
It relies on OS facilities:
| OS | Mechanism |
|---|---|
| Linux | epoll |
| macOS | kqueue |
| Windows | IOCP |
- Register socket with OS
- Ask: “tell me when readable/writable”
- OS blocks internally
- OS sends readiness event
- Runtime wakes the future
The thread is never blocked waiting for I/O.
Async I/O in Rust (API level)
Rust uses: AsyncRead , AsyncWrite
Traits (simplified):
trait AsyncRead {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<()>;
}
They follow the same Future polling rules:
- Return
Pendingif not ready - Register waker
- Return
Readywhen data is available
Tokio async TCP example (server)
Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
Async TCP echo server
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn handle_client(mut socket: TcpStream) {
let mut buf = [0; 1024];
loop {
let n = socket.read(&mut buf).await.unwrap();
if n == 0 {
break; // client disconnected
}
socket.write_all(&buf[..n]).await.unwrap();
}
}
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
loop {
let (socket, _) = listener.accept().await.unwrap();
tokio::spawn(handle_client(socket));
}
}
What happens step-by-step (deep explanation)
listener.accept().await
- Registers socket with reactor
- If no client → returns
Pending - OS waits for connection
- OS notifies runtime
- Runtime wakes task
- Accept resumes
socket.read().await
- Tries reading from socket
- If no data →
Pending - Registers waker
- OS waits for data
- OS signals readiness
- Task resumes
- Read completes
Multiple clients
- Each client = one task
- Tasks are lightweight
- Thousands of connections per thread
Non-blocking file I/O (important caveat)
Files are tricky
- Most OSes do not support async file IO well
- Tokio uses blocking threads under the hood
use tokio::fs;
let content = fs::read_to_string("file.txt").await.unwrap();
This is async from your POV, but internally:
- Offloaded to a thread pool
- Avoids blocking executor threads
Timers are also async I/O
use tokio::time::{sleep, Duration};
sleep(Duration::from_secs(1)).await;
Internally:
- Timer wheel / heap
- OS timer
- Waker triggered on timeout
What async I/O is NOT
- Parallel CPU work
- Faster computation
- Thread replacement
Async I/O is about waiting efficiently.
Common mistakes
Blocking calls in async code
std::fs::read("file.txt"); // blocks runtime
Holding locks across .await
let guard = mutex.lock().unwrap();
do_async().await; // DEADLOCK risk
Mixing runtimes
Tokio socket ≠ async-std socket.
Mental model to keep forever
Think of async I/O as:
“Register interest → yield → resume on readiness.”
Or:
“Don’t wait. Ask to be notified.”